DEBUG = False

from amplitude import xpath     # XPath extension functions
from amplitude.dict import Dict
from amplitude.lib import xsl
from amplitude.lib.id import random_id
from lxml import etree
import os, re, time, sys

TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
XSLT_QUOTED_STRING_PARAM_TYPE = type(etree.XSLT.strparam(""))

class XML(Dict):
    root_tag = None
    ns_prefix = None
    nsmap = None

    def __init__(self, xml, config=None, parser=None):
        self.config = config
        if type(xml)==XML:                                      # xml is an XML instance.
            for k in xml.keys():
                self[k] = xml[k]
        elif os.path.exists(xml):                               # xml is a file on the filesystem
            self.fn = xml
            self.tree = etree.parse(xml, parser=parser)
            self.root = self.tree.getroot()
        elif type(xml) in [str, unicode] and xml.strip() != ''\
        and xml[:5].lower()=='<?xml':                           # xml is a source data as a string/unicode
            self.root = etree.fromstring(xml, parser=parser)
        elif re.match("^\w+://", xml):                          # xml is a url -- get the network resource
            import urllib
            resource = urllib.urlopen(xml)  # this is a very naive, blocking approach to this problem.
            t = resource.read()             # how could we handle this better? ultimately we have to wait.
            resource.close()
            self.root = etree.fromstring(t, parser=parser)
        elif self.root_tag is not None:                         # default to a new root tag
            self.root = etree.Element(self.root_tag)
        else:
            self.root = etree.Element(self.__class__.__name__.lower())
        # replace doc.root.nsmap with something more useful
        self.nsmap = self.root.nsmap
        if None in self.root.nsmap.keys() and self.ns_prefix is not None:
            self.nsmap[ns_prefix] = self.nsmap.pop(None)
        

    def get_xml(self, root=None, config=None, encoding='UTF-8', xml_declaration=True, pretty_print=True, method='xml'):
        r = root or self.root
        return etree.tostring(r, encoding=encoding, xml_declaration=xml_declaration, pretty_print=pretty_print, method=method)
            
    def write(self, fn=None, xslfn=None, config=None, encoding='UTF-8', xml_declaration=True, pretty_print=True, cache_xslt=True, method='xml', xsl_params={}):
        outfn = fn or self.fn
        if method=='text':
            outfn = os.path.splitext(outfn)[0]+'.txt'
        if DEBUG==True: print self.__class__.__name__+'.write(), fn =', outfn
        if xslfn is not None:
            self.root = self.xslt(xslfn=xslfn, config=config, cache=cache_xslt, xsl_params=xsl_params)
        xml = self.get_xml(config=config, encoding=encoding, xml_declaration=xml_declaration, pretty_print=pretty_print)
        if method=='text':
            xml = re.sub("<[^>]+>", '', xml)
        if not os.path.isdir(os.path.dirname(outfn)):
            os.makedirs(os.path.dirname(outfn))
        try:
            f=open(outfn, 'wb')
        except: # sometimes there's a disk error on SSD
            import time
            time.sleep(.2)
            f=open(outfn, 'wb')
        xml = xml.replace(' xmlns=""', '')  # how to keep these silly things from showing up during XSLT usage?
        f.write(xml)
        f.close()

    def xslt(self, xslfn=None, config=None, elem=None, cache=True, xsl_params={}):
        if elem is None: elem = self.root
        config = config or self.config
        xslfn = find_xslfn(xslfn, config)
        # prepare string parameters -- see http://lxml.de/xpathxslt.html#stylesheet-parameters
        if 'timestamp' not in xsl_params.keys():     # always include a timestamp param
            xsl_params['timestamp'] = time.strftime(TIMESTAMP_FORMAT)
        params = {}
        for key in xsl_params:
            if type(xsl_params[key]) in [str, unicode]:
                params[key] = etree.XSLT.strparam(xsl_params[key])
            else:
                params[k] = xsl_params[key]
        if DEBUG==True: print xsl_params
        if xslfn is not None:
            xt = make_xslt(xslfn, cache=cache)
            return xt(elem, **params)

    def assertValid(self, tag=None):
        validator = self.validator or self.Validator(tag=tag)
        if validator is not None:
            validator.assertValid(self.root)
    
    def validate(self, tag=None, validator=None):
        validator = self.validator or self.Validator(tag=tag, validator=validator)
        if validator is not None:
            result = validator.validate(self.root)
            return result or False
            
    def Validator(self, tag=None, validator=None):
        fn = self.get_schema_path(tag)
        if fn is not None:
            if os.path.exists(fn):
                self.validator = self.get_validator(fn, validator)
            else:
                fn = os.path.join(os.path.dirname(fn)+','+os.path.basename(fn.split(',')[-1]))
                self.validator = self.get_validator(fn, validator)
            return self.validator

    def get_schema_path(self, tag=None):
        tag = tag or self.root_tag or self.root.tag
        if tag is not None and self.config.Repository.path is not None:
            fb = XML.tag_to_schema_file_basename(tag)    # map namespaces to schema filenames
            # 1. try a filename of the format <schema_dir>/fb/tag.rng - subdirectories are preferable
            # 2. try a filename of the format <schema_dir>/fb,tag.rng - filename prefix also supported
            fn = os.path.join(self.config.Repository.path, self.config.Repository.schemadir or 'schemas', fb + '.rng').replace("\\", '/')
            return fn

    def get_validator(self, fn, validator=None):
        if validator:
            return validator
        else:
            return etree.RelaxNG(etree.parse(fn))

    @classmethod
    def tag_to_schema_file_basename(cls, tag):
        return tag.replace('http://','').replace('/','_').replace('{','').replace('}','/')

    def write_html(self, fn=None, config=None, xslpath=None, xslfn=None, xslfns=[], encoding='UTF-8', xml_declaration=True, epub=False, xmlns=None, xsl_params={}):
        htmltext = self.render_html(xslfn=xslfn, xslpath=xslpath, xslfns=xslfns, encoding=encoding, xml_declaration=xml_declaration, epub=epub, xmlns=xmlns, xsl_params=xsl_params)
        if fn is None:
            fn = os.path.splitext(self.fn)[0] + '.html'
        if not os.path.isdir(os.path.dirname(fn)):
            os.makedirs(os.path.dirname(fn))
        f = open(fn, 'wb'); f.write(htmltext.encode(encoding)); f.close()
        return fn

    def render_html_body(self, xslfn=None, config=None, xml_declaration=False, xsl_params={}):
        config = config or self.config
        if xslfn is None:
            xslfn = find_xslfn(os.path.join(config.Repository.path, config.Repository.xsldir, self.root.tag+'_to_html-body.xsl'), config)
        return self.render_html(xslfn=xslfn, xml_declaration=xml_declaration, xsl_params=xsl_params)

    def render_html_text(self, encoding='UTF-8', xsl_params={}):
        h = self.html(encoding=encoding, xsl_params=xsl_params)
        return etree.tounicode(h, method='text')
        
    def render_html(self, xslpath=None, xslfn=None, config=None, xslfns=[], encoding='UTF-8', xml_declaration=True, epub=False, xmlns=None, xsl_params={}):
        doc = self.html(xslfn=xslfn, xslpath=xslpath, xslfns=xslfns, encoding=encoding, xml_declaration=xml_declaration, epub=epub, xmlns=xmlns, xsl_params=xsl_params)
        h = etree.tostring(doc, encoding=encoding, xml_declaration=xml_declaration) or ''
        # remove any xmlns="" attributes
        h = h.decode(encoding).replace(' xmlns=""', '')
        return h
        
    def html(self, xslpath=None, xslfn=None, config=None, xslfns=[], encoding='UTF-8', xml_declaration=True, epub=False, xmlns=None, cache_xslt=True, xsl_params={}):
        """convert this XML tree to HTML
        use xslpath if given; otherwise, get it from self.config if given
        use xslfns if given; otherwise, use xslfn if given; otherwise, fall back to the correct xslfn in xslpath
        """
        xsl_quoted_params = {}
        for k in xsl_params: xsl_quoted_params[k] = xsl_params[k]
        if 'timestamp' not in xsl_quoted_params.keys():
            xsl_quoted_params['timestamp'] = time.strftime(TIMESTAMP_FORMAT)
        for k in xsl_quoted_params.keys():
            if DEBUG==True: print k, xsl_quoted_params[k]
            if type(xsl_quoted_params[k]) != XSLT_QUOTED_STRING_PARAM_TYPE:
                xsl_quoted_params[k] = etree.XSLT.strparam(xsl_quoted_params[k])
        root_tag = re.sub("^\{[^\}]+\}", "", self.root.tag)
        config = config or self.config  
        xslfn = find_xslfn(xslfn, config)
        if xslpath is None and config is not None:
            xslpath = os.path.join(config.Repository.path, config.Repository.xsldir)
        if xslfn is None and xslpath is not None:
            if epub==True: 
                xslfb = root_tag+'_to_epub.xsl'
            else:
                xslfb = root_tag+'_to_html.xsl'
            xslfn = find_xslfn(os.path.join(xslpath, xslfb), config)
        if xslfns != []:
            xslts = [(type(fn) in [str, unicode]) and make_xslt(fn, cache=cache_xslt) or fn for fn in xslfns]
        else:
            xslts = []
        xslts.insert(0, make_xslt(xslfn, cache=cache_xslt))
        doc = self.root
        if DEBUG==True: print self.fn
        for xslt in xslts:
            doc = xslt(doc, **xsl_quoted_params)
        if xmlns not in ["", None] and doc.get('xmlns') is None:
            doc.set('xmlns', xmlns)
        return doc

    def to_word(self, config=None, fn=None, varclass=None, xslfn=None, xsl_params={}, redline=True, includes=False, imgs=False, 
                meta=True, tags=True, template=None, postproc=None, close=True):
        """convert the given file to Word"""
        from .word import Word
        from .text import Text
        if config is None: config=self.config
        assert config is not None
        htmlfn = os.path.splitext(self.fn)[0]+'.html'
        h = XML(self.fn, config=self.config)
        h.fn = htmlfn
        if includes in [True,'True']:
            incl_xslfn = os.path.join(config.Repository.path, config.Repository.xsldir, "include_items.xsl")
            h.root = h.xslt(xslfn=incl_xslfn, xsl_params=xsl_params).getroot()
            h.write()
            h = XML(h.fn, config=h.config)
        if varclass is not None:
            varclass_xslfn = os.path.join(config.Repository.path, config.Repository.xsldir, "items_filter_by_varclass.xsl")
            h.root = h.xslt(xslfn=varclass_xslfn, xsl_params={'varclass': varclass}).getroot()
            h.write()
            h = XML(h.fn, config=h.config)
        if xslfn is not None:
            h.root = h.xslt(xslfn=xslfn, xsl_params=xsl_params).getroot()
            h.write()
            h = XML(h.fn, config=h.config)
        main_xslfn = os.path.join(config.Repository.path, config.Repository.xsldir, h.root.tag+"_to_word.xsl")
        h.root = etree.XSLT(etree.parse(main_xslfn))(h.root).getroot()
        h.write(xml_declaration=False)
        t = Text(h.fn)
        t.text = t.text.replace("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n""", '')
        if imgs in [False, 'False']:
            t.text = re.sub("<img([^>]*)/>", "{{img\\1/}}", t.text)
        t.write()
        doc = Word(t.fn)
        doc.app.ActiveDocument.UpdateStylesOnOpen = True
        archive_file_dotpath = os.path.join(config.Repository.path,  config.Repository.templates or 'templates', "archive-file.dotm").replace('/','\\')
        if os.path.exists(archive_file_dotpath):
            doc.app.ActiveDocument.AttachedTemplate = archive_file_dotpath
            if meta in [False, 'False']:
                doc.app.Run("RemoveMeta")
            if tags in [False, 'False']:
                doc.app.Run("RemoveTags")
            else:
                doc.app.Run("TagsWithStyle")
        if template is not None:
            dotpath = os.path.abspath(os.path.join(config.Repository.path,  config.Repository.templates or 'templates', template))
            doc.app.ActiveDocument.AttachedTemplate = dotpath
        if redline in [True, 'True']:
            doc.app.ActiveDocument.TrackRevisions = True
        if postproc is not None:
            postproc(doc)
        if fn is not None and not os.path.exists(os.path.dirname(fn)):
            os.makedirs(os.path.dirname(fn))
        doc.save(fn=fn, format="Word")
        if close==True: doc.close()
        if DEBUG!=True: os.remove(htmlfn)
        

    def num_words(self):
        t = etree.tounicode(self.root, method="text").strip()
        words = re.split("\s+", t)
        return len(words)
        
    def tag_dict(self, tags={}, exclude_attribs=[]):
        """returns a dict of the tags and comments that occur in an XML"""
        for elem in self.root.xpath("//*"):
            if elem.tag not in tags:
                tags[elem.tag] = {}
            for a in elem.attrib:
                if a not in exclude_attribs:
                    if a not in tags[elem.tag]:
                        tags[elem.tag][a] = []
                    if elem.get(a) not in tags[elem.tag][a]:
                        tags[elem.tag][a].append(elem.get(a))
        for comment in self.root.xpath("//comment()"):
            c = str(comment).strip("<>")
            if c not in tags:
                tags[c] = {}
        return tags

    @classmethod
    def replace_with_contents(c, elem):
        "removes an element and leaves its contents in its place. Namespaces supported."
        parent = elem.getparent()
        index = parent.index(elem)
        children = elem.getchildren()
        previous = elem.getprevious()
        # text
        if index==0:
            parent.text = (parent.text or '') + (elem.text or '')
        else:
            previous.tail = (previous.tail or '') + (elem.text or '')
        # children
        for child in children:
            parent.insert(index + children.index(child), child)
        # tail
        if len(children) > 0:
            last_child = children[-1]
            last_child.tail = (last_child.tail or '') + (elem.tail or '')
        else:
            if index==0:
                parent.text = (parent.text or '') + (elem.tail or '')
            else:
                previous.tail = (previous.tail or '') + (elem.tail or '')
        # elem
        parent.remove(elem)

    @classmethod
    def apply_xslts(cls, infn, outfn, xslfns, encoding='UTF-8', xml_declaration=True, xsl_params={}, cache=True):
        """apply one or more xslt stylesheets to a file."""
        doc = cls(infn)
        if type(xslfns) in [str, unicode]: xslfns = [xslfns]
        if DEBUG==True: print ' ', xsl_params
        xsl_params = {k:etree.XSLT.strparam(xsl_params[k]) for k in xsl_params}
        for xslfn in xslfns:        
            xslt = make_xslt(xslfn, cache=cache)
            if DEBUG==True: print ' ', xslfn
            doc.root = xslt(doc.root, **xsl_params).getroot()
            if DEBUG==True: 
                doc.write(fn=outfn, encoding=encoding, xml_declaration=xml_declaration)
        doc.write(fn=outfn, encoding=encoding, xml_declaration=xml_declaration)

        
    # XPATH FUNCTIONS
    @classmethod
    def uppercase(c, elems):
        for elem in elems:
            elem.text = (elem.text or '').upper()
            for ch in elem.getchildren():
                XML.uppercase([ch])
                ch.tail = (ch.tail or '').upper()
        return elems

    @classmethod
    def lowercase(c, elems):
        for elem in elems:
            elem.text = (elem.text or '').lower()
            for ch in elem.getchildren():
                XML.lowercase([ch])
                ch.tail = (ch.tail or '').lower()
        return elems        

    def unpack_included_items(self, remove_empty=True):
        "put included items back into their source locations"
        for incl in self.root.xpath("//include_items"):
            srcfn = os.path.abspath(os.path.join(
                        os.path.dirname(self.fn), incl.get('src')))
            if os.path.exists(srcfn):
                src = XML(srcfn, self.config)
            else:
                src = XML("", self.config)
                src.fn = srcfn
                src.root.tag = 'items'
                src.root.text= '\n\n'
            print ' ', src.fn
            index = -1
            for item in incl.xpath("item"):
                index += 1
                item.tail = '\n\n'  # I like double-space between items
                src_items = src.root.xpath("//item[@name='%(name)s' and @typename='%(typename)s' and @product='%(product)s']" % item.attrib)
                if len(src_items)==1:
                    src_item = src_items[0]
                    parent = src_item.getparent()
                    if parent is None:
                        src.root = item
                    else:
                        parent.replace(src_item, item)
                elif len(src_items)==0:
                    if len(src_items) > index:
                        src.root.insert(index, item)
                    else:
                        src.root.append(item)
                if item.getparent()==incl: incl.remove(item)
                #print '    ', item.get('name')
            if incl.text is not None and incl.text.strip()=='': incl.text = None
            if remove_empty == True:
                ii = src.root.xpath("//item[body[not(*)]]")
                for i in ii:
                    i.getparent().remove(i)
            src.write(fn=src.fn)
            try: 
                src.assertValid()
            except:
                print >> sys.stderr, sys.exc_info()[1]

        if DEBUG==True: self.write()
        try: 
            self.assertValid()
        except: 
            print >> sys.stderr, sys.exc_info()[1]


XMLFile = XML   # for backward compatibility


def register_xpath_functions(functions=[], namespace=None):
    ns = etree.FunctionNamespace(namespace)
    for fn in functions:
        ns[fn.__name__] = fn

register_xpath_functions([XML.uppercase, XML.lowercase], "http://repository.tyndale.com/amplitude/xpath-functions")


def find_xslfn(xslfn, config=None):
    if type(xslfn) in [str, unicode]:
        if os.path.isfile(xslfn):
            return xslfn
        elif config is not None and config.Repository is not None:
            fb = os.path.basename(xslfn)
            xslfn = os.path.abspath(os.path.join(config.Repository.path, config.Repository.xsldir, fb)).replace('\\','/')
            if os.path.isfile(xslfn):
                return xslfn

# cache xslts for performance if DEBUG==False
XSLTS = {}
def make_xslt(xslfn, cache=True):
    if xslfn not in XSLTS or cache==False or DEBUG==False:
        xslt = etree.XSLT(etree.parse(xslfn))
        if DEBUG==False: XSLTS[xslfn] = xslt
    else:
        xslt = XSLTS[xslfn]
    return xslt
